﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Index.Interfaces;

namespace Index.TokenMap
{
    // format of the token map segment file is:
    // file is in binary format
    // file name is the current optimized generation and max ID for the segment, 
    // separated by dash gen-maxid.tmap (padded with leading zeros: 0000000002-0000000234.tmap)
    // all records contain an ID (uint) and TOKEN (string)
    // TOKEN is lower case text

    // index directory can contain multipe token map segments
    // all token map segments will be searched

    // when adding a group of tokens - new segment file is created

    // optimizeAsNeeded operation merges multiple segment files into one
    
    public class TokenMap : IIndexComponent, ITokenMap, IMergeable
    {
        public const string FileExtension = ".btmap";
        public const string FileTempExtension = ".btmaptmp";
        public const string SegmentFileNamePattern = "*" + FileExtension;
        public static readonly string PathSeparator = new string(Path.DirectorySeparatorChar, 1);

        private string indexDirectoryPath = null;

        public int MaxSegments { get; set; }

        public TokenMap()
        {
            this.MaxSegments = 100;
            this.OldSegmentDeleteRetryCount = 100;
            this.OldSegmentDeleteDelay = TimeSpan.FromMilliseconds(50);
        }

        public void Initialize(string indexDirectoryPath)
        {
            this.indexDirectoryPath = indexDirectoryPath;
        }

        public virtual void Cleanup()
        {
            string [] segments = GetSegments();
            DeleteOldSegments(segments, FindCurrentGeneration(segments));
        }

        public virtual void Dispose()
        {
            Cleanup();
        }

        #region ITokenMap interface

        public virtual bool FindTokenId(ref IToken token)
        {
            // get all segment files
            List<string> segments = GetCurrentGenerationSegments(GetSegments());

            // go through all allSegments starting with the last one (the most recent)
            for (int k = segments.Count - 1; k >= 0; k--)
            {
                using (FileStream fileStream = File.Open(segments[k], FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    using (BinaryReader binaryReader = new BinaryReader(fileStream))
                    {
                        // go through all lines to find the token
                        long length = fileStream.Length;

                        while (fileStream.Position < length)
                        {
                            uint id = binaryReader.ReadUInt32();
                            string tokenText = binaryReader.ReadString();
                        
                            if(tokenText == token.Text)
                            {
                                token.Id = id;
                                return true;
                            }
                        }
                    }
                }
            }

            return false;
        }

        public virtual void AddTokens(ref List<IToken> tokens, bool optimizeAsNeeded = true)
        {
            tokens.Sort(new TokenTextComparer());

            List<IToken> newTokens = new List<IToken>();

            lock (this)
            {
                uint maxTokenId = FindMaxTokenId();
                uint id;

                for (int k = 0; k < tokens.Count; k++)
                {
                    IToken token = tokens[k];
                    bool isFound = this.FindTokenId(ref token);

                    if (!isFound)
                    {
                        token.Id = ++maxTokenId;
                        newTokens.Add(token);
                    }
                }

                if (newTokens.Count > 0)
                {
                    CreateSegment(newTokens, maxTokenId, optimizeAsNeeded);
                }
            }
        }

        public virtual void Optimize()
        {
            lock (this)
            {
                string[] allSegments = GetSegments();
                List<string> segments = GetCurrentGenerationSegments(allSegments);
                List<SegmentName> segmentNames = GetSegmentNames(segments);
                uint maxTokenId = FindMaxTokenId(segmentNames);
                uint currentGeneration = FindCurrentGeneration(segmentNames);

                SegmentName optimizedSegment = new SegmentName();
                optimizedSegment.MaxTokenId = maxTokenId;
                optimizedSegment.Generation = CalculateNextGeneration(currentGeneration);

                string optimizedSegmentPath = this.indexDirectoryPath;

                if (!optimizedSegmentPath.EndsWith(PathSeparator))
                {
                    optimizedSegmentPath += PathSeparator;
                }

                optimizedSegmentPath += optimizedSegment.ToString();

                using (FileStream optimizedFileStream = File.Open(optimizedSegmentPath + FileTempExtension, FileMode.Create, FileAccess.Write, FileShare.Read))
                {
                    using (BinaryWriter optimizedBinaryWriter = new BinaryWriter(optimizedFileStream))
                    {
                        foreach (string segment in segments)
                        {
                            using(FileStream sourceFileStream = File.OpenRead(segment))
                            {
                                using(BinaryReader sourceBinaryReader = new BinaryReader(sourceFileStream))
                                {
                                    long length = sourceFileStream.Length;

                                    while (sourceFileStream.Position < length)
                                    {
                                        uint id = sourceBinaryReader.ReadUInt32();
                                        string tokenText = sourceBinaryReader.ReadString();

                                        optimizedBinaryWriter.Write(id);
                                        optimizedBinaryWriter.Write(tokenText);
                                    }
                                }
                            }
                        }
                    }
                }

                File.Move(optimizedSegmentPath + FileTempExtension, optimizedSegmentPath + FileExtension);

                Cleanup();
            }
        }


        #endregion ITokenMap interface


        #region IMergeable Members

        public void Merge(string indexDirectoryToMerge, int chunkSize = 10000)
        {
            lock (this)
            {
                string[] allSourceSegments = GetSegments(indexDirectoryToMerge);
                List<string> sourceSegments = GetCurrentGenerationSegments(allSourceSegments);

                foreach (string sourceSegment in sourceSegments)
                {
                    using(FileStream sourceFileStream = File.OpenRead(sourceSegment))
                    {
                        using(BinaryReader sourceBinaryReader = new BinaryReader(sourceFileStream))
                        {
                            List<IToken> tokens = new List<IToken>();

                            long length = sourceFileStream.Length;

                            while(sourceFileStream.Position < length)
                            {
                                Token token = new Token();
                                token.Id = sourceBinaryReader.ReadUInt32();
                                token.Text = sourceBinaryReader.ReadString();
                                tokens.Add(token);

                                if (tokens.Count >= chunkSize)
                                {
                                    this.AddTokens(ref tokens, false);
                                    tokens.Clear();
                                }
                            }

                            if(tokens.Count > 0)
                            {
                                this.AddTokens(ref tokens, false);
                                tokens.Clear();
                            }
                        }
                    }
                }            
            
                Optimize();
            }
        }

        #endregion

        protected virtual string[] GetSegments()
        {
            return GetSegments(this.indexDirectoryPath);
        }

        protected virtual string[] GetSegments(string indexDirectoryPath)
        {
            string[] segments = Directory.GetFiles(indexDirectoryPath, SegmentFileNamePattern);
            Array.Sort(segments);
            return segments;
        }

        protected virtual List<string> GetCurrentGenerationSegments(string[] segments)
        {
            List<string> currentGenerationSegments = new List<string>();
            uint currentGeneration = FindCurrentGeneration(segments);

            foreach (string segment in segments)
            {
                SegmentName segmentName = new SegmentName(Path.GetFileNameWithoutExtension(segment));

                if(segmentName.Generation == currentGeneration)
                {
                    currentGenerationSegments.Add(segment);
                }
            }

            return currentGenerationSegments;
        }

        protected virtual List<string> GetOldGenerationSegments(string[] segments)
        {
            List<string> oldGenerationSegments = new List<string>();
            uint currentGeneration = FindCurrentGeneration(segments);

            foreach (string segment in segments)
            {
                SegmentName segmentName = new SegmentName(Path.GetFileNameWithoutExtension(segment));

                if (segmentName.Generation != currentGeneration)
                {
                    oldGenerationSegments.Add(segment);
                }
            }

            return oldGenerationSegments;
        }


        protected virtual void CreateSegment(List<IToken> tokens, uint maxTokenId, bool optimizeAsNeeded = true)
        {
            tokens.Sort(new TokenTextComparer());

            lock(this)
            {
                string[] segments = GetSegments();

                string segmentPath = this.indexDirectoryPath;

                if (!segmentPath.EndsWith(PathSeparator))
                {
                    segmentPath += PathSeparator;
                }

                SegmentName segmentName = new SegmentName();
                segmentName.MaxTokenId = maxTokenId;
                segmentName.Generation = FindCurrentGeneration(segments);

                segmentPath += segmentName.ToString();

                using(FileStream segmentStream = File.OpenWrite(segmentPath + FileTempExtension))
                {
                    using(BinaryWriter segmentBinaryWriter = new BinaryWriter(segmentStream))
                    {
                        foreach (IToken token in tokens)
                        {
                            segmentBinaryWriter.Write(token.Id);
                            segmentBinaryWriter.Write(token.Text);
                        }
                    }
                }

                File.Move(segmentPath + FileTempExtension, segmentPath + FileExtension);

                if (optimizeAsNeeded)
                {
                    if (segments.Length >= this.MaxSegments)
                    {
                        Optimize();
                    }
                }
            }
        }

        protected virtual uint FindMaxTokenId(string[] segments = null)
        {
            uint maxId = 0;

            if (segments == null)
            {
                // get all segment files - file name is the max ID in the file
                segments = GetSegments();
            }

            // go through all allSegments
            foreach (string segment in segments)
            {
                string fileName = Path.GetFileNameWithoutExtension(segment);
                SegmentName segmentName = new SegmentName(fileName);
                maxId = Math.Max(maxId, segmentName.MaxTokenId);
            }

            return maxId;
        }

        protected virtual uint FindMaxTokenId(List<SegmentName> segmentNames)
        {
            uint maxId = 0;

            foreach (SegmentName segmentName in segmentNames)
            {
                maxId = Math.Max(maxId, segmentName.MaxTokenId);
            }

            return maxId;
        }


        public int OldSegmentDeleteRetryCount { get; set; }
        public TimeSpan OldSegmentDeleteDelay { get; set; }

        protected virtual void DeleteOldSegments(string[] allSegments, uint currentGeneration)
        {
            List<string> oldGenerationSegments = GetOldGenerationSegments(allSegments);

            foreach (string oldGenerationSegment in oldGenerationSegments)
            {
                int count = 0;

                while (File.Exists(oldGenerationSegment))
                {
                    try
                    {
                        File.Delete(oldGenerationSegment);
                    }
                    catch (Exception)
                    {
                        Thread.Sleep(this.OldSegmentDeleteDelay);
                    }

                    if(count++ > this.OldSegmentDeleteRetryCount)
                    {
                        break;
                    }
                }
            }
        }


        protected virtual uint CalculateNextGeneration(uint currentGeneration)
        {
            if(currentGeneration < uint.MaxValue)
            {
                return currentGeneration + 1;
            }
            else
            {
                return 0;
            }
        }

        protected virtual List<SegmentName> GetSegmentNames(string[] segments)
        {
            List<SegmentName> segmentNames = new List<SegmentName>();

            foreach (string segment in segments)
            {
                string fileName = Path.GetFileNameWithoutExtension(segment);
                SegmentName segmentName = new SegmentName(fileName);
                segmentNames.Add(segmentName);
            }

            return segmentNames;
        }

        protected virtual List<SegmentName> GetSegmentNames(List<string> segments)
        {
            List<SegmentName> segmentNames = new List<SegmentName>();

            foreach (string segment in segments)
            {
                string fileName = Path.GetFileNameWithoutExtension(segment);
                SegmentName segmentName = new SegmentName(fileName);
                segmentNames.Add(segmentName);
            }

            return segmentNames;
        }

        protected virtual uint FindCurrentGeneration(string[] segments)
        {
            List<SegmentName> segmentNames = GetSegmentNames(segments);
            return FindCurrentGeneration(segmentNames);
        }

        protected virtual uint FindCurrentGeneration(List<SegmentName> segmentNames)
        {
            uint maxGeneration = 0;
            uint minGeneration = uint.MaxValue;

            foreach (SegmentName segmentName in segmentNames)
            {
                maxGeneration = Math.Max(maxGeneration, segmentName.Generation);
                minGeneration = Math.Min(minGeneration, segmentName.Generation);
            }

            if(maxGeneration == uint.MaxValue && minGeneration == 0)
            {
                // count reached the max value and started over with 0
                // so the last generation is 0
                return 0;
            }
            else
            {
                return maxGeneration;
            }
        }


    }
}